查看原文
其他

驱动调试—还原 QQ 过滤驱动对关键内核设施所做的修改(Part II)

shayi 看雪学院 2019-05-26



在Part I 中,我们已经处理完最棘手的部分:杀掉 QQFrmMgr.sys 创建的系统线程。剩余的工作就轻松多了——移除 QQFrmMgr.sys 和 QQProtect.sys


安装的 SSDT(系统服务调度表)钩子与 SSDT Shadow 钩子、销毁它们注册的事件通知 callback,从而将系统恢复至干净状态。


在此之前,按照惯例,还是先来检查一下这两个 QQ 驱动是否attach到了其它设备栈中的设备上,因为 rootkit 或恶意软件通常会挂载到其它合法驱动创建的设备上,以便拦截或修改途经的 IRP(I/O 请求包)中携带的敏感数据,比如用户的击键数据。


如果发现了任何挂载迹象,则可以通过前一篇介绍的 APC 机制结合 IoDetachDevice() 例程,把恶意设备从设备栈中清理掉。

由于这两个 QQ 驱动会向 Windows 对象管理器维护的全局名称空间中,注册相应的设备对象名,如下图所示:



Windows I/O 管理器导出的两个例程 IoCreateDevice() 与 IoCreateSymbolicLink() 普遍被驱动程序用来向 NT 名称空间中注册设备对象名,以及相应的符号链接。


用户态进程使用符号链接访问该对象;而在内核空间中,可以直接通过对象名进行访问,所以我们先用内核调试器的 “!devstack” 扩展命令,后接这两个 QQ 设备对象的名称,查询它们是否挂载到了任何系统现存的设备栈上:



如你所见,这两个设备对象各自所在的设备栈中,都只有它们自身——如果它们挂载到了任何其它设备上,“!devstack” 的输出中就会含有那些 “受害” 的设备。


其中,“!DevObj” 栏位下的 4 字节 16 进制数是该设备对象的 “nt!_DEVICE_OBJECT” 结构地址;“!DrvObj” 栏位下的则是创建它们的驱动对象名称。


其实这两个 QQ 设备对象还算是 “良性” 的——某些 rootkit 创建设备对象时,根本不注册名字到 NT 名称空间(通过向 IoCreateDevice() 的第三个参数传入 NULL,就可以做到这一点),对于此类 “恶性” 的匿名设备,需要获悉它的 “nt!_DEVICE_OBJECT” 结构地址,然后才能用 “!devstack” 遍历设备栈,这个难度就不小了。


言归正传,接下来先检查系统的 SSDT,寻找有无被挂钩的系统服务,如下图,SSDT 的起始地址为 0x83c80f7c,一共有 0x191(401)个系统服务,其中一部分已经被替换成 QQFrmMgr.sys 的钩子函数:

  


本来可以利用 “!chkimg” 扩展命令执行自动化检查,将 nt 模块(ntoskrnl.exe)的内存映像与磁盘文件比较,从而找出那些被修改了的部分,但不知为何我的宿主机上 WinDbg 无法对 nt 模块实施检查,总是提示 ntkrpamp.exe/ntoskrnl.exe 的版本不匹配。(还请成功执行 “!chkimg” 命令检查 nt 模块的各位提供经验)


一种最原始的方法就是先记录下受感染机器上 QQFrmMgr.sys 的钩子函数在 SSDT 中的位置,然后把它与另一个干净系统上的 SSDT 对比,以得知被 hook的是哪个系统服务——前面那张截图就是用这种脏累的体力活实现的。


注意,系统每次初始化时,SSDT 的基地址,以及其中的系统服务入口点地址都是随机变化的,因而我们不能记录它们的内核地址,而是要记录函数名,在复原前用反汇编指令 “u” ,即可强制解析原始系统服务在本次启动时分配到的内核地址,然后以 “eb” 指令编辑还原被 hook 的表项——言词苍白无力,还是看图有真相吧:



注意,Intel x86/x64 体系结构的微处理器采用“小端字节序”在内存中放置数据,换言之,一个 “双字” (DWORD)数据的最低有效字节位于连续的四字节存储空间中的最小地址处;最高有效位则存放在最大地址处。以上图为例,系统服务 “nt!NtCreateSection” 的入口点地址——83e5583b,其中最低有效字节是 “3b”,所以我们在编辑时把它放在最前面(最小地址处)。


这个游戏规则是处理器硬件厂商规定的,如果不遵守它来办事就无法正确地恢复被挂钩的系统函数!


此外,通过分析我们还得知:QQFrmMgr.sys 利用 “inline hook” 技术硬挂钩了 KeUserModeCallback() 内核例程中的正常函数调用,由于我机器上的 “!chkimg” 不能工作,无法依赖它检测处挂钩前的原始函数调用,但是我们可以用 IDA PRO 逆向 ntkrpamp.exe/ntoskrnl.exe 的磁盘文件,定位到 KeUserModeCallback() 中的原始函数调用——这种不修改函数指针数组(如 SSDT,一般位于 .data section),而是修改特定函数(一般位于.text section)中的调用逻辑,就称为“inline hook”。


如下两张截图,手工比较 “ntkrnlpa.exe” 的磁盘文件与内存映像,两者的 KeUserModeCallback() 初始逻辑(机器指令序列)基本相同,除了内存映像中的首条 call 指令目标被替换成了 “QQFrmMgr+0x503e” 之外......

 

 


击败 “inline hook” 的方法也是用前面介绍过的 “eb” 内存编辑指令,根据上图 IDA PRO 中的原始信息来还原。同样需要留心字节序的问题!


(事实上,“!chkimg”有一个开关选项为“-f”,能够把二进制文件的内存映像中所有被篡改或损坏之处还原成与磁盘文件上的一致,“一键还原所有 hook”,无需前面介绍的繁杂手工操作;但既然该指令用在 nt 模块上如此令人蛋疼,这里也就无法演示了,请各位自行测试!)


清理完 SSDT 中的病毒后,让我们来关注 SSDT Shadow——内核全局变量 “KeServiceDescriptorTableShadow” 持有该表的基地址,该表由 Win32 子系统的内核模式部分—— Win32k.sys 填写,负责实现 GUI 线程请求的所有涉及窗口绘图,鼠标指针,以及其它图形操作。


由于多数正规应用程序为了与用户交互,都会请求图形操作,因此该表也成了 rootkit 们重点 hook 的系统数据结构之一。


如下图所示,我们在检查 SSDT Shadow 中的函数时遇到了无效的内存地址(显示为问号),这是因为断入调试器的当前线程不是一个 “GUI 线程” ,所以它的 SSDT Shadow 起始虚拟地址(图中的 9a94c000)没有映射到物理内存中的实际 SSDT Shadow(您可以看到描述该虚拟地址的页表条目是非法的):



如果当前线程的 “nt!_KTHREAD” 结构的 “Win32Thread” 字段为“NULL”,那么它就不是一个 GUI 线程,从而它的页表中,描述 SSDT Shadow 虚拟地址的那一项 PTE 就是无效的,没法用来转译物理地址:



为了“看见 ”Win32k.sys 实现的 SSDT Shadow,我们可以在绝大多数用户、内核模式线程都频繁调用的例程——nt!NtWaitForSingleObject()

下一个条件断点,仅在调用线程的 “Win32Thread” 字段不为空时,才断入内核调试器,这样我们就能够窥视 SSDT Shadow 了!


如下一系列截图所示:



综上截图所述,QQFrmMgr.sys 在 SSDT Shadow 中仅安装了一枚钩子,我们计算出该钩子例程在整个 Win32k.sys 内存映像中的偏移量为

0x20c630——该信息相当关键,它用于在 Win32k.sys 磁盘文件中定位原始的 SSDT Shadow 函数入口地址。


再次以 IDA PRO 打开 Win32k.sys,从该模块的描述信息可知,它的基地址为 0xBF800000,把这个值加上 0x20c630 得出0xBFA0C630——也就是持有 “受害” 函数入口点的位置,于是我们在 IDA PRO 中执行 “Jump to address” 菜单选项,跳转到该地址,真相大白,被 hook 的就是 “win32k!NtUserFindWindowEx” ,请注意它的前后两个例程与内存映像中钩子例程的前后两个完全一致。


反汇编 “win32k!NtUserFindWindowEx” 来获取它在本次引导实例中的内核地址,然后用 “eb” 移除掉可恶的 QQ 钩子函数,至此大功告成!



小结



本文演示了如何通过调试手段,将被恶意修改的关键系统设施还原成初始状态。


限于篇幅,最后一部分内容——销毁 QQ 驱动注册的事件通知回调函数——将放在 Part III 中介绍。







本文由看雪论坛 shayi  原创

转载请注明来自看雪社区



往期热门阅读:



扫描二维码关注我们,更多干货等你来拿!


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存